Type Narrowing 是一種概念,指在程式碼執行過程中,根據特定條件,將變數的型別從一個較廣泛的型別縮小為較特定的型別,以提供更精確的型別資訊,這種功能有助於提高程式碼的可讀性和可維護性,同時確保運行時更安全。( 有點類似使用 unknown 要先型別檢查 )。
優點:
提高程式碼可讀性
: 型別縮小使我們可以在程式碼中提供更多上下文信息,幫助其他開發者更容易理解。
增加型別安全性
: 型別縮小可以減少運行時發生錯誤的可能性,因為編譯器可以提前檢測到型別不相容的操作。
typeof 會回傳的類型有:string、number、boolean、object、function、undefined、bigint、symbol。
看以下範例:
const printValue = (value: string | number): void => {
const print =
typeof value === "string" ? `${value}好棒棒!` : value.toFixed(2);
console.log(print);
};
printValue("威爾豬"); // 輸出: 威爾豬好棒棒!
printValue(3); // 輸出: 3.00
在上面的範例中,typeof value === "string" 縮小了 value 變數的型別,因此我們可以安全地使用樣板字面值。
不過要特別注意 null,它的 typeof 值和陣列一樣是 object:
const printValue = (value: string | string[] | null): void => {
if (typeof value === "object") {
for (const item of value) {
console.log(item);
}
} else if (typeof value === "string") {
console.log(value);
} else {
console.log("這是 null");
}
};
printValue("威爾豬"); // 輸出: 威爾豬
printValue(null); // ❌ 錯誤,null 不能跑迴圈
我們可以修改上面的範例為:
const printValue = (value: string | string[] | null) {
if (value && typeof value === "object") {
for (const item of value) {
console.log(item);
}
} else if (typeof value === "string") {
console.log(value);
}else {
console.log("這是 null");
}
}
printValue("威爾豬"); // 輸出: 威爾豬
printValue(null); // ⭕️ 輸出: 這是 null
interface ICircle {
radius: number;
}
interface IRect {
width: number;
height: number;
}
type TShape = ICircle | IRect;
const getArea = (shape: TShape): void => {
let shapeText: string = "";
let area = 0;
if ("radius" in shape) {
shapeText = "圓形";
area = Math.round(Math.PI * shape.radius ** 2);
} else {
shapeText = "方形";
area = shape.width * shape.height;
}
console.log(`${shapeText}面積為 ${area}`);
};
const circle: ICircle = { radius: 10 };
const rect: IRect = { width: 2, height: 3 };
getArea(circle); // 輸出: 圓形面積為 314
getArea(rect); // 輸出: 方形面積為 6
在上面的範例中,"radius" in shape 縮小了 shape 的接口類型,所以我們可以明確的知道現在的形狀及面積。
instanceof 用来檢查一個值是否是另一個值的「實體 / 實例 ( Instance )」,也就是檢查建構函式的 prototype 屬性是否存在於某個實體的原型鏈上。
看以下範例:
class Animal {}
class Cat extends Animal {}
const makeSound = (animal: Animal): void => {
const sound = animal instanceof Cat ? "喵喵喵~" : "我不知道叫聲";
console.log(sound);
};
const myCat: Cat = new Cat();
makeSound(myCat); // 輸出: 喵喵喵~
非空斷言是 TypeScript 專屬的語法,這種表示法使用 !
放在變數名稱或屬性後面。我們可以使用非空斷言來縮小型別,表示 這個值的型別不會是 null 或 undefined
,並且希望 TypeScript 忽略可能的空值錯誤檢查。
const greet = (name?: string | null): void => {
console.log(`哈囉,${name!.trim()}!`);
};
greet(" 威爾豬"); // 輸出: 哈囉,威爾豬!
greet(); // ❌ 執行錯誤
上面範例我們將 name 參數使用非空斷言,表示我們跟 typescript 說這個值不會是 null 或 undefined,請忽略空值的檢查。
非空斷言要小心使用
,因為實際上變數為 null 或 undefined 時,在執行就會引發錯誤。
Type predicates 是使用 自定義帶有特殊的返回型別的函數
。
看以下範例:
interface ICat {
run(): void;
}
interface IBird {
swim(): void;
}
type TPet = ICat | IBird;
// 使用 type predicates 來確定 pet 是否是 ICat 接口
function isCat(pet: TPet): pet is ICat {
return "run" in pet;
}
const myPet = (pet: TPet): void => {
isCat(pet) ? pet.run() : pet.swim();
};
const myCat: ICat = {
run() {
console.log("我的貓在跑");
},
};
const myBird: IBird = {
swim() {
console.log("我的魚在游");
},
};
myPet(myCat); // 輸出: 我的貓在跑
myPet(myBird); // 輸出: 我的魚在游
我們使用具有相同名稱的屬性來區分不同的型別,這些屬性通常被稱為 標記
,每個型別都擁有唯一的標記值,因此 TypeScript 可以根據標記值來確定變數的正確型別。
範例如下:
interface ICircle {
kind: "circle";
radius: number;
}
interface IRect {
kind: "rect";
width: number;
height: number;
}
type TShape = ICircle | IRect;
function getArea(shape: TShape): number {
let area = 0;
// 使用 Discriminated unions 來判斷型別
switch (shape.kind) {
case "circle":
area = Math.round(Math.PI * shape.radius ** 2);
console.log(`圓形面積為 ${area}`);
return;
case "rect":
area = shape.width * shape.height;
console.log(`方形面積為 ${area}`);
return;
default:
throw new Error("沒有這個圖形");
}
}
const myCircle: ICircle = {
kind: "circle",
radius: 10,
};
const myRect: IRect = {
kind: "rect",
width: 2,
height: 3,
};
getArea(myCircle); // 輸出: 圓形面積為 314
getArea(myRect); // 輸出: 方形面積為 6
其實有時候我們不知不覺都在使用 Type Narrowing,它可以透過使用 typeof、instanceof、in 等條件限制來縮小變數的類型範圍,使 TypeScript 和其他開發者可以更好地理解變數的實際類型,並適當地進行型別檢查和推斷,我們可以在程式碼中充分利用這一功能,以提高開發效率並減少錯誤。